/* Copyright(C) 2006-2008 Dave Sexton  *
 * http://www.codeplex.com/aip         *
 * http://www.codeplex.com/aip/license *
 ***************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using DaveSexton.Web.Controls.Resources;

namespace DaveSexton.Web.Controls
{
	/// <summary>
	/// An implementation of <see cref="AutoInputProtectionImage"/> that renders the text generated by an implementation of 
	/// <see cref="AutoInputProtectionTextProvider"/> by segregating individual characters into even spaced veritcal partitions 
	/// and uses randomized colors, fonts and rotations.
	/// </summary>
	/// <remarks>
	/// <para>
	/// This class inherits from <see cref="BackgroundAutoInputProtectionImage"/> so that a background image may be used as the 
	/// surface image on which to draw the text.
	/// </para>
	/// <para>
	/// Instances of <see cref="PartitionedAutoInputProtectionImage"/> are created by <see cref="PartitionedAutoInputProtectionImageProvider"/>, 
	/// which can be inherited so that derived image provider implementations don't have to render text themselves.
	/// </para>
	/// </remarks>
	/// <include file='examples.xml' path='//example[@id="LineNoise"][@type="images"]'/>
	/// <include file='examples.xml' path='//example[@id="Resource"][@type="images"]'/>
	/// <seealso cref="PartitionedAutoInputProtectionImageProvider"/>
	/// <seealso cref="LineNoiseAutoInputProtectionImageProvider"/>
	/// <seealso cref="ResourceAutoInputProtectionImageProvider"/>
	public class PartitionedAutoInputProtectionImage : BackgroundAutoInputProtectionImage
	{
		#region Public Properties
		/// <summary>
		/// Gets or sets the left, right, bottom and top margins from the edges of the image in which the <see cref="Text" /> will be rendered, in pixels.
		/// </summary>
		/// <value>The margin in which the <see cref="Text"/> will be rendered, in pixels.  The default value is 2.</value>
		public int Margin
		{
			get
			{
				return margin;
			}
			set
			{
				margin = value;
			}
		}

		/// <summary>
		/// Gets or sets the minimum <see cref="Font"/> size, in pixels, for each character.
		/// </summary>
		/// <exception cref="ArgumentOutOfRangeException">The specified value is not a positive integer.</exception>
		/// <value>The minimum <see cref="Font"/> size, in pixels, to be generated at random.</value>
		public int MinimumFontSize
		{
			get
			{
				return minFontSize;
			}
			set
			{
				if (value < 1)
					throw new ArgumentOutOfRangeException("value", value, Errors.PositiveIntRequired);

				minFontSize = value;
			}
		}

		/// <summary>
		/// Gets or sets the maximum <see cref="Font"/> size, in pixels, for each character.
		/// </summary>
		/// <exception cref="ArgumentOutOfRangeException">The specified value is not a positive integer.</exception>
		/// <value>The maximum <see cref="Font"/> size, in pixels, to be generated at random.</value>
		public int MaximumFontSize
		{
			get
			{
				return maxFontSize;
			}
			set
			{
				if (value < 1)
					throw new ArgumentOutOfRangeException("value", value, Errors.PositiveIntRequired);

				maxFontSize = value;
			}
		}

		/// <summary>
		/// Gets or sets the minimum amount, in degrees, of random rotation that is applied to each character.
		/// </summary>
		/// <value>The minimum amount, in degrees, of random rotation that is applied to each character.</value>
		public int MinimumCharacterRotation
		{
			get
			{
				return minRotation;
			}
			set
			{
				// negatives are allowed
				minRotation = value;
			}
		}

		/// <summary>
		/// Gets or sets the maximum amount, in degrees, of random rotation that is applied to each character.
		/// </summary>
		/// <value>The maximum amount, in degrees, of random rotation that is applied to each character</value>
		public int MaximumCharacterRotation
		{
			get
			{
				return maxRotation;
			}
			set
			{
				// negatives are allowed
				maxRotation = value;
			}
		}
		#endregion

		#region Private / Protected
		private int margin = 2;
		private int minFontSize, maxFontSize, minRotation = -35, maxRotation = 35;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="PartitionedAutoInputProtectionImage" /> class.
		/// </summary>
		/// <exception cref="ArgumentNullException">The specified <paramref name="textProvider"/> is <see langword="null"/>.</exception>
		/// <exception cref="ArgumentOutOfRangeException">The specified <paramref name="size"/> is less than the value of 
		/// <see cref="AutoInputProtectionImage.MinimumSize"/>.</exception>
		/// <param name="size">The dimensions of the <see cref="AutoInputProtectionImage.CompositeImage"/></param>
		/// <param name="textProvider">The <see cref="AutoInputProtectionTextProvider"/> implementation that generates the 
		/// <see cref="AutoInputProtectionImage.Text"/> for the <see cref="AutoInputProtectionImage.CompositeImage"/>.</param>
		public PartitionedAutoInputProtectionImage(Size size, AutoInputProtectionTextProvider textProvider)
			: base(size, textProvider)
		{
			if (textProvider == null)
				throw new ArgumentNullException("textProvider");

			minFontSize = textProvider.MinimumFontSize;
			maxFontSize = textProvider.MaximumFontSize;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="PartitionedAutoInputProtectionImage" /> class.
		/// </summary>
		/// <remarks>
		/// The dimensions of the specified <paramref name="background"/> image are used as the dimensions of the <see cref="AutoInputProtectionImage.CompositeImage"/>
		/// that will be generated.
		/// </remarks>
		/// <exception cref="ArgumentNullException">The specified <paramref name="background"/> image or <paramref name="textProvider"/> is <see langword="null"/>.</exception>
		/// <exception cref="ArgumentOutOfRangeException">The specified <paramref name="size"/> is less than the value of 
		/// <see cref="AutoInputProtectionImage.MinimumSize"/>.</exception>
		/// <param name="background">The <see cref="Image"/> that will be rendered in the background of the <see cref="AutoInputProtectionImage.CompositeImage"/>.</param>
		/// <param name="textProvider">The <see cref="AutoInputProtectionTextProvider"/> implementation that generates the 
		/// <see cref="AutoInputProtectionImage.Text"/> for the <see cref="AutoInputProtectionImage.CompositeImage"/>.</param>
		public PartitionedAutoInputProtectionImage(Image background, AutoInputProtectionTextProvider textProvider)
			: base(background, textProvider)
		{
			if (textProvider == null)
				throw new ArgumentNullException("textProvider");

			minFontSize = textProvider.MinimumFontSize;
			maxFontSize = textProvider.MaximumFontSize;
		}
		#endregion

		#region Methods
		/// <summary>
		/// Renders the <see cref="AutoInputProtectionImage.Text"/> to the specified <paramref name="graphics"/> object.
		/// </summary>
		/// <remarks>
		/// <para>
		/// This method is called by <see cref="AutoInputProtectionImage.CreateCompositeImage()"/> after all of the preprocess <see cref="AutoInputProtectionImage.Filters"/> have 
		/// been applied.
		/// </para>
		/// <para>
		/// The base implementation renders the text by segregating individual characters into even spaced veritcal partitions and 
		/// uses randomized fonts and rotations.
		/// </para>
		/// <para>
		/// See <see cref="PartitionedAutoInputProtectionImage"/> for examples.
		/// </para>
		/// </remarks>
		/// <exception cref="InvalidOperationException"><see cref="MaximumFontSize"/> is less than <see cref="MinimumFontSize"/> or 
		/// <see cref="MaximumCharacterRotation"/> is less than <see cref="MinimumCharacterRotation"/>.</exception>
		/// <seealso cref="AutoInputProtectionImage.CompositeImage"/>
		/// <seealso cref="AutoInputProtectionImage.CreateCompositeImage()"/>
		/// <seealso cref="GenerateCharacters"/>
		/// <seealso cref="Render"/>
		/// <param name="graphics"><see cref="Graphics"/> object that provides the surface on which to draw.</param>
		/// <param name="size">The dimensions of the image.</param>
		protected sealed override void CreateCompositeImage(Graphics graphics, Size size)
		{
			if (maxFontSize < minFontSize)
				throw new InvalidOperationException(Errors.PartitionedAIPImage_FontSizeMaxLessThanMin);

			if (maxRotation < minRotation)
				throw new InvalidOperationException(Errors.PartitionedAIPImage_RotationMaxLessThanMin);

			using (StringFormat format = new StringFormat(
				StringFormatFlags.NoClip | StringFormatFlags.NoFontFallback | StringFormatFlags.FitBlackBox))
			{
				Random random = new Random();

				//Debug.WriteLine("Text Length: " + Text.Length, "PartitionedAutoInputProtectionImage");

				float textWidth;
				IEnumerable<CharRenderInfo> characters = GenerateCharacters(graphics, random, format, out textWidth);

				//Debug.WriteLine("Text Width: " + textWidth, "PartitionedAutoInputProtectionImage");

				Render(graphics, size, random, characters, format, textWidth);
			}
		}

		/// <summary>
		/// Creates an <see cref="IEnumerable{T}"/> of <see cref="CharRenderInfo"/> objects that store information about 
		/// the individual characters in the text that will be rendered.
		/// </summary>
		/// <remarks>
		/// This method is used by <see cref="CreateCompositeImage"/> and the result is passed to <see cref="Render"/>.
		/// </remarks>
		/// <param name="graphics"><see cref="Graphics"/> object used to measure characters.</param>
		/// <param name="random">An instance of the <see cref="Random"/> class used to randomize <see cref="Font"/> and <see cref="Color"/> samples.</param>
		/// <param name="format">The <see cref="StringFormat"/> used to measure characters.</param>
		/// <param name="textWidth">An output argument that indicates the total width of the text when each character is drawn side by side.</param>
		/// <returns>An <see cref="IEnumerable{T}"/> of <see cref="CharRenderInfo"/> objects that store information about 
		/// the individual characters in the text that will be rendered.</returns>
		private IEnumerable<CharRenderInfo> GenerateCharacters(Graphics graphics, Random random, StringFormat format, out float textWidth)
		{
			// NOTE: Do not use a C# iterator here since textWidth must be calculated immediately
			textWidth = 0;

			string text = Text;
			IList<Color> colors = new List<Color>(TextColors);
			IList<Font> fonts = new List<Font>(Fonts);
			List<CharRenderInfo> generatedCharacters = new List<CharRenderInfo>(text.Length);

			for (int i = 0; i < text.Length; i++)
			{
				char c = text[i];

				Font font = new Font(fonts[random.Next(0, fonts.Count)].FontFamily, random.Next(minFontSize, maxFontSize + 1), GraphicsUnit.Pixel);

				SizeF size = graphics.MeasureString(c.ToString(), font, (int) graphics.ClipBounds.Width, format);

				textWidth += size.Width;

				generatedCharacters.Add(new CharRenderInfo(c, font, size, colors[random.Next(0, colors.Count)]));
			}

			return generatedCharacters;
		}

		/// <summary>
		/// Renders the specified <paramref name="characters"/> to the specified <paramref name="graphics"/> object in the specified <paramref name="format"/> and
		/// within the specified <paramref name="size"/>.
		/// </summary>
		/// <remarks>
		/// <para>
		/// This method is used to draw the text onto the surface image in even spaced veritcal partitions.  Character rotation and vertical placement is randomized.
		/// </para>
		/// </remarks>
		/// <seealso cref="GenerateCharacters"/>
		/// <seealso cref="CreateCompositeImage"/>
		/// <seealso cref="RenderCharacter"/>
		/// <param name="graphics"><see cref="Graphics"/> object that provides the surface on which the specified <paramref name="characters"/> are drawn.</param>
		/// <param name="size">The dimensions in which the specified <paramref name="characters"/> will be drawn.</param>
		/// <param name="random">An instance of the <see cref="Random"/> class used to randomize character rotation and vertical placement.</param>
		/// <param name="characters">The text to be drawn to the specified <paramref name="graphics"/> object.</param>
		/// <param name="format">The <see cref="StringFormat"/> used to render the specified <paramref name="characters"/>.</param>
		/// <param name="textWidth">Indicates the total width of the text when the specified <paramref name="characters"/> are drawn side by side.</param>
		private void Render(Graphics graphics, Size size, Random random, IEnumerable<CharRenderInfo> characters, StringFormat format, float textWidth)
		{
			string text = Text;

			// calculate the position at which the current character will be drawn on the x-axis
			int x = (text.Length == 1)
				? (int) (size.Width / 2 - textWidth / 2)
				: margin;

			// calculate the step length between characters on the x-axis
			int stepX = (text.Length == 1)
				? 0
				: (int) (size.Width - textWidth - margin * 2) / (text.Length - 1);

			//Debug.WriteLine("Char Step (x-axis): " + stepX, "PartitionedAutoInputProtectionImage");

			foreach (CharRenderInfo c in characters)
			// render characters from left to right
			{
				Font font = c.Font;
				int rotate = random.Next(minRotation, maxRotation + 1);
				int maxY = size.Height - font.Height - margin * 2;
				int y = (margin >= maxY) ? margin : random.Next(margin, maxY + 1);

				//Debug.WriteLine("Char Location: " + new Point(x, y).ToString(), "PartitionedAutoInputProtectionImage");
				//Debug.WriteLine("Char Rotation: " + rotate, "PartitionedAutoInputProtectionImage");

				GraphicsContainer container = graphics.BeginContainer();

				/* Debug partition
				using (SolidBrush brush = new SolidBrush(Color.FromArgb(75, c.Color)))
				{
					graphics.FillRectangle(brush, x, 0, c.Size.Width, size.Height);
				}
				*/

				graphics.TranslateTransform(x + c.Size.Width / 2, y + c.Size.Height / 2);
				graphics.RotateTransform(rotate);
				graphics.TranslateTransform(-c.Size.Width / 2, -c.Size.Height / 2);

				RenderCharacter(graphics, c.Char, c.Size, c.Font, c.Color, format, random);

				graphics.EndContainer(container);

				x += (int) c.Size.Width + stepX;
			}
		}

		/// <summary>
		/// Renders the specified <paramref name="character"/> to the specified <paramref name="graphics"/> object in the specified 
		/// <paramref name="font"/>, <paramref name="color"/> and <paramref name="format"/>.
		/// </summary>
		/// <remarks>
		/// <para>
		/// This method is called by the <see cref="Render"/> method once per <paramref name="character"/>.
		/// </para>
		/// <para>
		/// The <paramref name="size"/> and <paramref name="random"/> arguments are not used by the base implementation; however, 
		/// they may be used when this method is overriden by derived types.
		/// </para>
		/// </remarks>
		/// <seealso cref="Render"/>
		/// <param name="graphics"><see cref="Graphics"/> object that provides the surface on which the specified <paramref name="character"/> is drawn.</param>
		/// <param name="character">The text to be drawn to the specified <paramref name="graphics"/> object.</param>
		/// <param name="size">The dimensions of the specified <paramref name="character"/>.</param>
		/// <param name="font">The <see cref="Font"/> in which the specified <paramref name="character"/> is drawn.</param>
		/// <param name="color">The <see cref="Color"/> in which the specified <paramref name="character"/> is drawn.</param>
		/// <param name="format">The <see cref="StringFormat"/> used to render the specified <paramref name="character"/>.</param>
		/// <param name="random">An instance of the <see cref="Random"/> class.</param>
		protected virtual void RenderCharacter(Graphics graphics, char character, SizeF size, Font font, Color color, StringFormat format, Random random)
		{
			/* Debug character region
			using (Brush brush = new SolidBrush(Color.FromArgb(50, Color.Green)))
			{
				graphics.FillRectangle(brush, 0, 0, size.Width, size.Height);
			}
			*/

			using (SolidBrush brush = new SolidBrush(color))
			{
				graphics.DrawString(character.ToString(), font, brush, 0, 0, format);
			}
		}
		#endregion

		#region Nested
		/// <summary>
		/// Contains information about an individual character in the text that will be rendered by an instance of <see cref="PartitionedAutoInputProtectionImage"/>.
		/// This class cannot be inherited.
		/// </summary>
		private sealed class CharRenderInfo
		{
			#region Public Properties
			/// <summary>
			/// Gets the <paramref name="char"/> that an instance of this class represents.
			/// </summary>
			public char Char
			{
				get
				{
					return c;
				}
			}

			/// <summary>
			/// Gets the <see cref="Font"/> in which the character will be drawn.
			/// </summary>
			public Font Font
			{
				get
				{
					return font;
				}
			}

			/// <summary>
			/// Gets the <see cref="SizeF"/> that is the dimensions of the character.
			/// </summary>
			public SizeF Size
			{
				get
				{
					return size;
				}
			}

			/// <summary>
			/// Gets the <see cref="Color"/> in which the character will be drawn.
			/// </summary>
			public Color Color
			{
				get
				{
					return color;
				}
			}
			#endregion

			#region Private / Protected
			private readonly char c;
			private readonly Font font;
			private readonly SizeF size;
			private readonly Color color;
			#endregion

			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="CharRenderInfo" /> class.
			/// </summary>
			/// <param name="c">The <paramref name="char"/> that the new instance represents.</param>
			/// <param name="font">The <see cref="Font"/> in which the character will be drawn.</param>
			/// <param name="size">The <see cref="SizeF"/> that is the dimensions of the character.</param>
			/// <param name="color">The <see cref="Color"/> in which the character will be drawn.</param>
			public CharRenderInfo(char c, Font font, SizeF size, Color color)
			{
				this.c = c;
				this.font = font;
				this.size = size;
				this.color = color;
			}
			#endregion
		}
		#endregion
	}
}
